This notebook compares doublet results calculated on benchmarking datasets to one another, with the primary goal of addressing these questions:

  1. Do methods tend to predict overlapping or distinct sets of doublets on the same dataset?
  2. Is the consensus doublet call across methods predictive of the true doublet status? Here, the “consensus doublets” are those droplets which all methods identify as doublets.

There are several items to bear in mind when interpreting these results:

Setup

Packages

suppressPackageStartupMessages({
  library(SingleCellExperiment)
  library(ggplot2)
  library(patchwork)
  library(caret)
  library(UpSetR)
})

theme_set(theme_bw())

Paths

module_base <- rprojroot::find_root(rprojroot::is_renv_project)
data_dir <- file.path(module_base, "scratch", "benchmark-datasets")
result_dir <- file.path(module_base, "results", "benchmark-results")

Functions

plot_pca_calls <- function(df, 
                           color_column, 
                           pred_column,
                           color_lab) {
  # Plot PCs colored by singlet/doublet, showing doublets on top
  # df is expected to contain columns PC1, PC2, `color_column`, and `pred_column`. These should _not_ be provided as strings
  ggplot(df) + 
    aes(x = PC1, 
        y = PC2, 
        color = {{color_column}}) +
  geom_point(
    size = 0.75, 
    alpha = 0.6
  ) +
  scale_color_manual(name = color_lab, values = c("black", "lightblue")) + 
  geom_point(
    data = dplyr::filter(df, {{color_column}} == "doublet"), 
    color = "black",
    size = 0.75
  ) +
  theme(
    legend.title.position = "top",
    legend.position = "bottom"
  )
}

plot_pca_metrics <- function(df, color_column, false_colors) {
  # Plot PCs colored by performance metric, showing false calls on top
  # false_colors is a vector of colors used for fn and fp points
  # df is expected to contain columns PC1, PC2, and `color_column`. This should _not_ be provided as a string.
  ggplot(df) + 
    aes(x = PC1, 
        y = PC2, 
        color = {{color_column}}) +
  geom_point(
    size = 0.75, 
    alpha = 0.6
  ) + 
  geom_point(
    data = dplyr::filter(df, {{color_column}} %in% false_colors), 
    size = 0.75
  ) +
  scale_color_identity() +
  theme(legend.position = "none")
}

Read and prepare input data

First, we’ll read in and combine doublet results into a list of data frames for each dataset. We’ll also create new columns for each dataset:

# find all dataset names to process:
dataset_names <- list.files(result_dir, pattern = "*_scrublet.tsv") |>
  stringr::str_remove("_scrublet.tsv")
# used in PCA plots
confusion_colors <- c(
  "tp" = "lightblue",
  "tn" = "pink",
  "fp" = "blue",
  "fn" = "firebrick2"
)

# Read in and data for analysis
doublet_df_list <- dataset_names |>
  purrr::map(
    \(dataset) {
      
      scdbl_tsv <- file.path(result_dir, glue::glue("{dataset}_scdblfinder.tsv"))
      scrub_tsv <- file.path(result_dir, glue::glue("{dataset}_scrublet.tsv"))
      sce_file <- file.path(data_dir, dataset, glue::glue("{dataset}_sce.rds"))
      
      scdbl_df <- scdbl_tsv |>
        readr::read_tsv(show_col_types = FALSE) |>
        dplyr::select(
          barcodes,
          cxds_score, 
          scdbl_score = score, 
          scdbl_prediction  = class
        ) |>
        # add cxds calls at 0.75 threshold
        dplyr::mutate(
          cxds_prediction = dplyr::if_else(
            cxds_score >= 0.75,
            "doublet",
            "singlet"
          )
        ) 
      
      scrub_df <- readr::read_tsv(scrub_tsv, show_col_types = FALSE) 

      # grab ground truth and PCA coordinates
      sce <- readr::read_rds(sce_file)
      sce_df <- scuttle::makePerCellDF(sce, use.dimred = "PCA") |>
        tibble::rownames_to_column(var = "barcodes") |>
        dplyr::select(barcodes,
                      ground_truth = ground_truth_doublets, 
                      PC1 = PCA.1, 
                      PC2 = PCA.2)
      rm(sce)
      
      dataset_df <- scdbl_df |>
        dplyr::left_join(
          scrub_df, 
          by = "barcodes"
        ) |>
        dplyr::left_join(
          sce_df, 
          by = "barcodes"
        ) 
      
      # Add a consensus call column
      dataset_df <- dataset_df |>
        dplyr::rowwise() |>
        dplyr::mutate(consensus_call = dplyr::if_else(
          all(
            c(scdbl_prediction, scrublet_prediction, cxds_prediction) == "doublet"
          ),
          "doublet", 
          "singlet"
        )) |>
        dplyr::mutate(
          call_type = dplyr::case_when(
            consensus_call == "doublet" && ground_truth == "doublet" ~ "tp",
            consensus_call == "singlet" && ground_truth == "singlet" ~ "tn",
            consensus_call == "doublet" && ground_truth == "singlet" ~ "fp",
            consensus_call == "singlet" && ground_truth == "doublet" ~ "fn"
          ), 
          # set associated plotting colors
          call_type_color = dplyr::case_when(
            call_type == "tp" ~ unname(confusion_colors["tp"]),
            call_type == "tn" ~ unname(confusion_colors["tn"]),
            call_type == "fp" ~ unname(confusion_colors["fp"]),
            call_type == "fn" ~ unname(confusion_colors["fn"])
          )
          )
      
      return(dataset_df)
    }
  )
names(doublet_df_list) <- dataset_names

Upset plots

This section contains upset plots that show overlap across doublet calls from each method, displayed for each dataset.

pull_barcodes <- function(df, pred_var) {
  # Helper function to pull out barcodes for doublet calls
  df$barcodes[df[[pred_var]] == "doublet"]
}

upset_list <- doublet_df_list |>
  purrr::iwalk(
    \(df, dataset) {
      
      doublet_barcodes <- list(
        "scDblFinder" = pull_barcodes(df, "scdbl_prediction"),
        "scrublet"    = pull_barcodes(df, "scrublet_prediction"),
        "cxds"        = pull_barcodes(df, "cxds_prediction")
      )
      
      UpSetR::upset(fromList(doublet_barcodes), order.by = "freq") |> print()
      grid::grid.text( # plot title
        dataset,
        x = 0.65, 
        y = 0.95, 
        gp = grid::gpar(fontsize=16)
      ) 

    }
  )

Evaluating consensus performance

This section visualizes and evaluates the consensus doublet calls across each dataset.

PCA

This section plots the PCA for each dataset, with three color schemes from left to right:

  1. Ground truth doublets are shown in black and singlets in blue
  2. Consensus doublets are shown in black and all remaining droplets in blue
  3. Points are colored based on comparing the consensus call to the ground truth as one of:
  • true positive (tp), true negative (tn), false positive (fp), false negative (fn)
# Make a legend for the confusion-colored PCA
legend_plot <- data.frame(
  x = factor(names(confusion_colors), levels = names(confusion_colors)), y = 1:4
) |>
 ggplot(aes(x = x, y = y, color = x)) + 
  geom_point(size = 3) + 
  scale_color_manual(name = "Metric", values = confusion_colors) 
confusion_legend <- ggpubr::get_legend(legend_plot) |> ggpubr::as_ggplot()

doublet_df_list |>
  purrr::iwalk(
    \(df, dataset) {
      
      # First, ground truth
      p1 <- plot_pca_calls(
        df, 
        color_column = ground_truth, 
        color_lab = "Ground truth"
      )
      
      # Second, consensus call
      p2 <- plot_pca_calls(
        df, 
        color_column = consensus_call, 
        color_lab = "Consensus call"
      )
      
      # Third, call type
      p3 <- plot_pca_metrics(
        df,
        call_type_color,
        false_colors = unname(c(confusion_colors["fn"], confusion_colors["fp"]))
      )

      # combine and plot
      plot( p1 + p2 + p3 + confusion_legend + plot_annotation(glue::glue("PCA for {dataset}")) + plot_layout(ncol=4, widths = c(1,1,1,0.25)) )
    }
  )

Performance metrics

This section calculates a confusion matrix and associated statistics on the consensus calls.

metric_df <- doublet_df_list |>
  purrr::imap( 
    \(df, dataset) {
        print(glue::glue("======================== {dataset} ========================"))
      
        cat("Table of consensus calls:")
        print(table(df$consensus_call))
        
        cat("\n\n")
        
        confusion_result <- caret::confusionMatrix(
          # truth should be first
          table(
            "Truth" = df$ground_truth,
            "Consensus prediction" = df$consensus_call
          ), 
          positive = "doublet"
        ) 
        
        print(confusion_result)
        
        # Extract information we want to present later in a table
        tibble::tibble(
          "Dataset name" = dataset,
          "Kappa" = round(confusion_result$overall["Kappa"], 3), 
          "Balanced accuracy" = round(confusion_result$byClass["Balanced Accuracy"], 3)
        )
    }
  ) |>
  dplyr::bind_rows()
======================== hm-6k ========================
Table of consensus calls:
doublet singlet 
     62    6744 


Confusion Matrix and Statistics

         Consensus prediction
Truth     doublet singlet
  doublet      62     109
  singlet       0    6635
                                          
               Accuracy : 0.984           
                 95% CI : (0.9807, 0.9868)
    No Information Rate : 0.9909          
    P-Value [Acc > NIR] : 1               
                                          
                  Kappa : 0.5258          
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
            Sensitivity : 1.00000         
            Specificity : 0.98384         
         Pos Pred Value : 0.36257         
         Neg Pred Value : 1.00000         
             Prevalence : 0.00911         
         Detection Rate : 0.00911         
   Detection Prevalence : 0.02512         
      Balanced Accuracy : 0.99192         
                                          
       'Positive' Class : doublet         
                                          
======================== HMEC-orig-MULTI ========================
Table of consensus calls:
doublet singlet 
    247   26179 


Confusion Matrix and Statistics

         Consensus prediction
Truth     doublet singlet
  doublet     204    3364
  singlet      43   22815
                                         
               Accuracy : 0.8711         
                 95% CI : (0.867, 0.8751)
    No Information Rate : 0.9907         
    P-Value [Acc > NIR] : 1              
                                         
                  Kappa : 0.0911         
                                         
 Mcnemar's Test P-Value : <2e-16         
                                         
            Sensitivity : 0.825911       
            Specificity : 0.871500       
         Pos Pred Value : 0.057175       
         Neg Pred Value : 0.998119       
             Prevalence : 0.009347       
         Detection Rate : 0.007720       
   Detection Prevalence : 0.135019       
      Balanced Accuracy : 0.848705       
                                         
       'Positive' Class : doublet        
                                         
======================== pbmc-1B-dm ========================
Table of consensus calls:
doublet singlet 
     13    3777 


Confusion Matrix and Statistics

         Consensus prediction
Truth     doublet singlet
  doublet       8     122
  singlet       5    3655
                                         
               Accuracy : 0.9665         
                 95% CI : (0.9603, 0.972)
    No Information Rate : 0.9966         
    P-Value [Acc > NIR] : 1              
                                         
                  Kappa : 0.1063         
                                         
 Mcnemar's Test P-Value : <2e-16         
                                         
            Sensitivity : 0.615385       
            Specificity : 0.967699       
         Pos Pred Value : 0.061538       
         Neg Pred Value : 0.998634       
             Prevalence : 0.003430       
         Detection Rate : 0.002111       
   Detection Prevalence : 0.034301       
      Balanced Accuracy : 0.791542       
                                         
       'Positive' Class : doublet        
                                         
======================== pdx-MULTI ========================
Table of consensus calls:
doublet singlet 
      4   10292 


Confusion Matrix and Statistics

         Consensus prediction
Truth     doublet singlet
  doublet       3    1314
  singlet       1    8978
                                          
               Accuracy : 0.8723          
                 95% CI : (0.8657, 0.8787)
    No Information Rate : 0.9996          
    P-Value [Acc > NIR] : 1               
                                          
                  Kappa : 0.0038          
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
            Sensitivity : 0.7500000       
            Specificity : 0.8723280       
         Pos Pred Value : 0.0022779       
         Neg Pred Value : 0.9998886       
             Prevalence : 0.0003885       
         Detection Rate : 0.0002914       
   Detection Prevalence : 0.1279138       
      Balanced Accuracy : 0.8111640       
                                          
       'Positive' Class : doublet         
                                          

Conclusions

Overall, methods do not have substantial overlap with out another. They each tend to detect different sets of doublets, leading to fairly small sets of consensus doublets. Further, the consensus doublets called by all three methods have some, but not substantial, overlap with the ground truth.

For three out of four datasets, scDblFinder predicts a much larger number of doublets compared to other methods. For pdx-MULTI, however, cxds predicts a much larger number of droplets.

The table below summarizes performance of the “consensus caller”. Note that, in the benchmarking paper these datasets were originally analyzed in, hm-6k was observed to be one of the “easiest” datasets to classify across methods.
Consistent with that observation, it has the highest kappa value here, although it is still fairly low - though not as low as the other methods, which are very close to 0.

metric_df

Session Info

# record the versions of the packages used in this analysis and other environment information
sessionInfo()
R version 4.4.0 (2024-04-24)
Platform: aarch64-apple-darwin20
Running under: macOS Sonoma 14.5

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/New_York
tzcode source: internal

attached base packages:
[1] stats4    stats     graphics  grDevices datasets  utils     methods  
[8] base     

other attached packages:
 [1] UpSetR_1.4.0                caret_6.0-94               
 [3] lattice_0.22-6              patchwork_1.2.0            
 [5] ggplot2_3.5.1               SingleCellExperiment_1.26.0
 [7] SummarizedExperiment_1.34.0 Biobase_2.64.0             
 [9] GenomicRanges_1.56.0        GenomeInfoDb_1.40.0        
[11] IRanges_2.38.0              S4Vectors_0.42.0           
[13] BiocGenerics_0.50.0         MatrixGenerics_1.16.0      
[15] matrixStats_1.3.0          

loaded via a namespace (and not attached):
  [1] pROC_1.18.5               gridExtra_2.3            
  [3] rlang_1.1.3               magrittr_2.0.3           
  [5] e1071_1.7-14              compiler_4.4.0           
  [7] DelayedMatrixStats_1.26.0 vctrs_0.6.5              
  [9] reshape2_1.4.4            stringr_1.5.1            
 [11] pkgconfig_2.0.3           crayon_1.5.2             
 [13] fastmap_1.1.1             backports_1.4.1          
 [15] XVector_0.44.0            labeling_0.4.3           
 [17] scuttle_1.14.0            utf8_1.2.4               
 [19] rmarkdown_2.26            tzdb_0.4.0               
 [21] prodlim_2023.08.28        UCSC.utils_1.0.0         
 [23] bit_4.0.5                 purrr_1.0.2              
 [25] xfun_0.43                 beachmat_2.20.0          
 [27] zlibbioc_1.50.0           cachem_1.0.8             
 [29] jsonlite_1.8.8            recipes_1.0.10           
 [31] highr_0.10                DelayedArray_0.30.1      
 [33] BiocParallel_1.38.0       broom_1.0.5              
 [35] parallel_4.4.0            R6_2.5.1                 
 [37] bslib_0.7.0               stringi_1.8.4            
 [39] car_3.1-2                 parallelly_1.37.1        
 [41] rpart_4.1.23              lubridate_1.9.3          
 [43] jquerylib_0.1.4           Rcpp_1.0.12              
 [45] iterators_1.0.14          knitr_1.46               
 [47] future.apply_1.11.2       readr_2.1.5              
 [49] Matrix_1.7-0              splines_4.4.0            
 [51] nnet_7.3-19               timechange_0.3.0         
 [53] tidyselect_1.2.1          abind_1.4-5              
 [55] yaml_2.3.8                timeDate_4032.109        
 [57] codetools_0.2-20          listenv_0.9.1            
 [59] tibble_3.2.1              plyr_1.8.9               
 [61] withr_3.0.0               evaluate_0.23            
 [63] future_1.33.2             survival_3.5-8           
 [65] proxy_0.4-27              ggpubr_0.6.0             
 [67] pillar_1.9.0              BiocManager_1.30.23      
 [69] carData_3.0-5             renv_1.0.7               
 [71] foreach_1.5.2             generics_0.1.3           
 [73] vroom_1.6.5               rprojroot_2.0.4          
 [75] hms_1.1.3                 sparseMatrixStats_1.16.0 
 [77] munsell_0.5.1             scales_1.3.0             
 [79] globals_0.16.3            class_7.3-22             
 [81] glue_1.7.0                tools_4.4.0              
 [83] data.table_1.15.4         ggsignif_0.6.4           
 [85] ModelMetrics_1.2.2.2      gower_1.0.1              
 [87] cowplot_1.1.3             grid_4.4.0               
 [89] tidyr_1.3.1               ipred_0.9-14             
 [91] colorspace_2.1-0          nlme_3.1-164             
 [93] GenomeInfoDbData_1.2.12   cli_3.6.2                
 [95] fansi_1.0.6               S4Arrays_1.4.0           
 [97] lava_1.8.0                dplyr_1.1.4              
 [99] gtable_0.3.5              rstatix_0.7.2            
[101] sass_0.4.9                digest_0.6.35            
[103] SparseArray_1.4.3         farver_2.1.1             
[105] htmltools_0.5.8.1         lifecycle_1.0.4          
[107] hardhat_1.3.1             httr_1.4.7               
[109] bit64_4.0.5               MASS_7.3-60.2            
LS0tCnRpdGxlOiAiQ29tcGFyaXNvbiBhbW9uZyBkb3VibGV0IHByZWRpY3Rpb25zIG9uIGJlbmNobWFya2luZyBkYXRhc2V0cyIKYXV0aG9yOiBTdGVwaGFuaWUgSi4gU3BpZWxtYW4KZGF0ZTogImByIFN5cy5EYXRlKClgIgpvdXRwdXQ6IAogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHRydWUKICAgIHRvY19kZXB0aDogNAogICAgY29kZV9mb2xkaW5nOiBoaWRlCi0tLQoKVGhpcyBub3RlYm9vayBjb21wYXJlcyBkb3VibGV0IHJlc3VsdHMgY2FsY3VsYXRlZCBvbiBiZW5jaG1hcmtpbmcgZGF0YXNldHMgdG8gb25lIGFub3RoZXIsIHdpdGggdGhlIHByaW1hcnkgZ29hbCBvZiBhZGRyZXNzaW5nIHRoZXNlIHF1ZXN0aW9uczoKCjEuIERvIG1ldGhvZHMgdGVuZCB0byBwcmVkaWN0IG92ZXJsYXBwaW5nIG9yIGRpc3RpbmN0IHNldHMgb2YgZG91YmxldHMgb24gdGhlIHNhbWUgZGF0YXNldD8KMi4gSXMgdGhlIGNvbnNlbnN1cyBkb3VibGV0IGNhbGwgYWNyb3NzIG1ldGhvZHMgcHJlZGljdGl2ZSBvZiB0aGUgdHJ1ZSBkb3VibGV0IHN0YXR1cz8KSGVyZSwgdGhlICJjb25zZW5zdXMgZG91YmxldHMiIGFyZSB0aG9zZSBkcm9wbGV0cyB3aGljaCBhbGwgbWV0aG9kcyBpZGVudGlmeSBhcyBkb3VibGV0cy4KClRoZXJlIGFyZSBzZXZlcmFsIGl0ZW1zIHRvIGJlYXIgaW4gbWluZCB3aGVuIGludGVycHJldGluZyB0aGVzZSByZXN1bHRzOgoKLSBUaGUgZ3JvdW5kIHRydXRoIGNhbGxzIHRoZW1zZWx2ZXMgdG8gd2hpY2ggd2UgYXJlIGNvbXBhcmluZyBjb25zZW5zdXMgY2FsbHMgbWF5IG5vdCBiZSBlbnRpcmVseSBhY2N1cmF0ZSwgc2luY2UgdGhleSB3ZXJlIGFsc28gY29tcHV0YXRpb25hbGx5IGlkZW50aWZpZWQgZ2VuZXJhbGx5IHdpdGggZGVtdWx0aXBsZXhpbmcgYWxnb3JpdGhtcy4gCi0gYGN4ZHNgLCBhcyB3ZSBhcmUgdXNpbmcgaXQsIGRvZXMgbm90IGhhdmUgYSBzcGVjaWZpYyB0aHJlc2hvbGQgZm9yIGNhbGxpbmcgZHJvcGxldHMuIApCeSBjb250cmFzdCwgYm90aCBgc2N1YmxldGAgYW5kIGBzY0RibEZpbmRlcmAgaWRlbnRpZnkgYSB0aHJlc2hvbGQgYmFzZWQgb24gdGhlIGdpdmVuIGRhdGFzZXQgdGhleSBhcmUgcHJvY2Vzc2luZy4gClRoaXMgbm90ZWJvb2sgdXNlZCBhIGRvdWJsZXQgdGhyZXNob2xkIG9mIGA+PTAuNzVgIGZvciBgY3hkc2AsIHdoaWNoIG1heSBub3QgYmUgdW5pdmVyc2FsbHkgc3VpdGFibGUgKHRob3VnaCBjaG9vc2luZyBhIHVuaXZlcnNhbGx5IHN1aXRhYmxlIHRocmVzaG9sZCBpcyBub3QgZWFzeSBlaXRoZXIhKS4KCiMjIFNldHVwCgojIyMgUGFja2FnZXMKCgpgYGB7ciBwYWNrYWdlc30Kc3VwcHJlc3NQYWNrYWdlU3RhcnR1cE1lc3NhZ2VzKHsKICBsaWJyYXJ5KFNpbmdsZUNlbGxFeHBlcmltZW50KQogIGxpYnJhcnkoZ2dwbG90MikKICBsaWJyYXJ5KHBhdGNod29yaykKICBsaWJyYXJ5KGNhcmV0KQogIGxpYnJhcnkoVXBTZXRSKQp9KQoKdGhlbWVfc2V0KHRoZW1lX2J3KCkpCmBgYAoKIyMjIFBhdGhzCgoKYGBge3IgYmFzZSBwYXRoc30KbW9kdWxlX2Jhc2UgPC0gcnByb2pyb290OjpmaW5kX3Jvb3QocnByb2pyb290Ojppc19yZW52X3Byb2plY3QpCmRhdGFfZGlyIDwtIGZpbGUucGF0aChtb2R1bGVfYmFzZSwgInNjcmF0Y2giLCAiYmVuY2htYXJrLWRhdGFzZXRzIikKcmVzdWx0X2RpciA8LSBmaWxlLnBhdGgobW9kdWxlX2Jhc2UsICJyZXN1bHRzIiwgImJlbmNobWFyay1yZXN1bHRzIikKYGBgCgojIyMgRnVuY3Rpb25zCgpgYGB7cn0KcGxvdF9wY2FfY2FsbHMgPC0gZnVuY3Rpb24oZGYsIAogICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xvcl9jb2x1bW4sIAogICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVkX2NvbHVtbiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgY29sb3JfbGFiKSB7CiAgIyBQbG90IFBDcyBjb2xvcmVkIGJ5IHNpbmdsZXQvZG91YmxldCwgc2hvd2luZyBkb3VibGV0cyBvbiB0b3AKICAjIGRmIGlzIGV4cGVjdGVkIHRvIGNvbnRhaW4gY29sdW1ucyBQQzEsIFBDMiwgYGNvbG9yX2NvbHVtbmAsIGFuZCBgcHJlZF9jb2x1bW5gLiBUaGVzZSBzaG91bGQgX25vdF8gYmUgcHJvdmlkZWQgYXMgc3RyaW5ncwogIGdncGxvdChkZikgKyAKICAgIGFlcyh4ID0gUEMxLCAKICAgICAgICB5ID0gUEMyLCAKICAgICAgICBjb2xvciA9IHt7Y29sb3JfY29sdW1ufX0pICsKICBnZW9tX3BvaW50KAogICAgc2l6ZSA9IDAuNzUsIAogICAgYWxwaGEgPSAwLjYKICApICsKICBzY2FsZV9jb2xvcl9tYW51YWwobmFtZSA9IGNvbG9yX2xhYiwgdmFsdWVzID0gYygiYmxhY2siLCAibGlnaHRibHVlIikpICsgCiAgZ2VvbV9wb2ludCgKICAgIGRhdGEgPSBkcGx5cjo6ZmlsdGVyKGRmLCB7e2NvbG9yX2NvbHVtbn19ID09ICJkb3VibGV0IiksIAogICAgY29sb3IgPSAiYmxhY2siLAogICAgc2l6ZSA9IDAuNzUKICApICsKICB0aGVtZSgKICAgIGxlZ2VuZC50aXRsZS5wb3NpdGlvbiA9ICJ0b3AiLAogICAgbGVnZW5kLnBvc2l0aW9uID0gImJvdHRvbSIKICApCn0KCnBsb3RfcGNhX21ldHJpY3MgPC0gZnVuY3Rpb24oZGYsIGNvbG9yX2NvbHVtbiwgZmFsc2VfY29sb3JzKSB7CiAgIyBQbG90IFBDcyBjb2xvcmVkIGJ5IHBlcmZvcm1hbmNlIG1ldHJpYywgc2hvd2luZyBmYWxzZSBjYWxscyBvbiB0b3AKICAjIGZhbHNlX2NvbG9ycyBpcyBhIHZlY3RvciBvZiBjb2xvcnMgdXNlZCBmb3IgZm4gYW5kIGZwIHBvaW50cwogICMgZGYgaXMgZXhwZWN0ZWQgdG8gY29udGFpbiBjb2x1bW5zIFBDMSwgUEMyLCBhbmQgYGNvbG9yX2NvbHVtbmAuIFRoaXMgc2hvdWxkIF9ub3RfIGJlIHByb3ZpZGVkIGFzIGEgc3RyaW5nLgogIGdncGxvdChkZikgKyAKICAgIGFlcyh4ID0gUEMxLCAKICAgICAgICB5ID0gUEMyLCAKICAgICAgICBjb2xvciA9IHt7Y29sb3JfY29sdW1ufX0pICsKICBnZW9tX3BvaW50KAogICAgc2l6ZSA9IDAuNzUsIAogICAgYWxwaGEgPSAwLjYKICApICsgCiAgZ2VvbV9wb2ludCgKICAgIGRhdGEgPSBkcGx5cjo6ZmlsdGVyKGRmLCB7e2NvbG9yX2NvbHVtbn19ICVpbiUgZmFsc2VfY29sb3JzKSwgCiAgICBzaXplID0gMC43NQogICkgKwogIHNjYWxlX2NvbG9yX2lkZW50aXR5KCkgKwogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikKfQpgYGAKCgojIyBSZWFkIGFuZCBwcmVwYXJlIGlucHV0IGRhdGEKCkZpcnN0LCB3ZSdsbCByZWFkIGluIGFuZCBjb21iaW5lIGRvdWJsZXQgcmVzdWx0cyBpbnRvIGEgbGlzdCBvZiBkYXRhIGZyYW1lcyBmb3IgZWFjaCBkYXRhc2V0LgpXZSdsbCBhbHNvIGNyZWF0ZSBuZXcgY29sdW1ucyBmb3IgZWFjaCBkYXRhc2V0OgoKLSBgY29uc2Vuc3VzX2NhbGxgLCB3aGljaCB3aWxsIGJlICJkb3VibGV0IiBpZiBfYWxsXyBtZXRob2RzIHByZWRpY3QgImRvdWJsZXQsIiBhbmQgInNpbmdsZXQiIG90aGVyd2lzZQotIGBjYWxsX3R5cGVgLCB3aGljaCB3aWxsIGNsYXNzaWZ5IHRoZSBjb25zZW5zdXMgY2FsbCBhcyBvbmUgb2YgInRwIiwgInRuIiwgImZwIiwgb3IgImZuIiAodHJ1ZS9mYWxzZSBwb3NpdGl2ZS9uZWdhdGl2ZSkgCgpgYGB7ciBwYXRoc30KIyBmaW5kIGFsbCBkYXRhc2V0IG5hbWVzIHRvIHByb2Nlc3M6CmRhdGFzZXRfbmFtZXMgPC0gbGlzdC5maWxlcyhyZXN1bHRfZGlyLCBwYXR0ZXJuID0gIipfc2NydWJsZXQudHN2IikgfD4KICBzdHJpbmdyOjpzdHJfcmVtb3ZlKCJfc2NydWJsZXQudHN2IikKYGBgCgpgYGB7ciByZWFkX2RhdGF9CiMgdXNlZCBpbiBQQ0EgcGxvdHMKY29uZnVzaW9uX2NvbG9ycyA8LSBjKAogICJ0cCIgPSAibGlnaHRibHVlIiwKICAidG4iID0gInBpbmsiLAogICJmcCIgPSAiYmx1ZSIsCiAgImZuIiA9ICJmaXJlYnJpY2syIgopCgojIFJlYWQgaW4gYW5kIGRhdGEgZm9yIGFuYWx5c2lzCmRvdWJsZXRfZGZfbGlzdCA8LSBkYXRhc2V0X25hbWVzIHw+CiAgcHVycnI6Om1hcCgKICAgIFwoZGF0YXNldCkgewogICAgICAKICAgICAgc2NkYmxfdHN2IDwtIGZpbGUucGF0aChyZXN1bHRfZGlyLCBnbHVlOjpnbHVlKCJ7ZGF0YXNldH1fc2NkYmxmaW5kZXIudHN2IikpCiAgICAgIHNjcnViX3RzdiA8LSBmaWxlLnBhdGgocmVzdWx0X2RpciwgZ2x1ZTo6Z2x1ZSgie2RhdGFzZXR9X3NjcnVibGV0LnRzdiIpKQogICAgICBzY2VfZmlsZSA8LSBmaWxlLnBhdGgoZGF0YV9kaXIsIGRhdGFzZXQsIGdsdWU6OmdsdWUoIntkYXRhc2V0fV9zY2UucmRzIikpCiAgICAgIAogICAgICBzY2RibF9kZiA8LSBzY2RibF90c3YgfD4KICAgICAgICByZWFkcjo6cmVhZF90c3Yoc2hvd19jb2xfdHlwZXMgPSBGQUxTRSkgfD4KICAgICAgICBkcGx5cjo6c2VsZWN0KAogICAgICAgICAgYmFyY29kZXMsCiAgICAgICAgICBjeGRzX3Njb3JlLCAKICAgICAgICAgIHNjZGJsX3Njb3JlID0gc2NvcmUsIAogICAgICAgICAgc2NkYmxfcHJlZGljdGlvbiAgPSBjbGFzcwogICAgICAgICkgfD4KICAgICAgICAjIGFkZCBjeGRzIGNhbGxzIGF0IDAuNzUgdGhyZXNob2xkCiAgICAgICAgZHBseXI6Om11dGF0ZSgKICAgICAgICAgIGN4ZHNfcHJlZGljdGlvbiA9IGRwbHlyOjppZl9lbHNlKAogICAgICAgICAgICBjeGRzX3Njb3JlID49IDAuNzUsCiAgICAgICAgICAgICJkb3VibGV0IiwKICAgICAgICAgICAgInNpbmdsZXQiCiAgICAgICAgICApCiAgICAgICAgKSAKICAgICAgCiAgICAgIHNjcnViX2RmIDwtIHJlYWRyOjpyZWFkX3RzdihzY3J1Yl90c3YsIHNob3dfY29sX3R5cGVzID0gRkFMU0UpIAoKICAgICAgIyBncmFiIGdyb3VuZCB0cnV0aCBhbmQgUENBIGNvb3JkaW5hdGVzCiAgICAgIHNjZSA8LSByZWFkcjo6cmVhZF9yZHMoc2NlX2ZpbGUpCiAgICAgIHNjZV9kZiA8LSBzY3V0dGxlOjptYWtlUGVyQ2VsbERGKHNjZSwgdXNlLmRpbXJlZCA9ICJQQ0EiKSB8PgogICAgICAgIHRpYmJsZTo6cm93bmFtZXNfdG9fY29sdW1uKHZhciA9ICJiYXJjb2RlcyIpIHw+CiAgICAgICAgZHBseXI6OnNlbGVjdChiYXJjb2RlcywKICAgICAgICAgICAgICAgICAgICAgIGdyb3VuZF90cnV0aCA9IGdyb3VuZF90cnV0aF9kb3VibGV0cywgCiAgICAgICAgICAgICAgICAgICAgICBQQzEgPSBQQ0EuMSwgCiAgICAgICAgICAgICAgICAgICAgICBQQzIgPSBQQ0EuMikKICAgICAgcm0oc2NlKQogICAgICAKICAgICAgZGF0YXNldF9kZiA8LSBzY2RibF9kZiB8PgogICAgICAgIGRwbHlyOjpsZWZ0X2pvaW4oCiAgICAgICAgICBzY3J1Yl9kZiwgCiAgICAgICAgICBieSA9ICJiYXJjb2RlcyIKICAgICAgICApIHw+CiAgICAgICAgZHBseXI6OmxlZnRfam9pbigKICAgICAgICAgIHNjZV9kZiwgCiAgICAgICAgICBieSA9ICJiYXJjb2RlcyIKICAgICAgICApIAogICAgICAKICAgICAgIyBBZGQgYSBjb25zZW5zdXMgY2FsbCBjb2x1bW4KICAgICAgZGF0YXNldF9kZiA8LSBkYXRhc2V0X2RmIHw+CiAgICAgICAgZHBseXI6OnJvd3dpc2UoKSB8PgogICAgICAgIGRwbHlyOjptdXRhdGUoY29uc2Vuc3VzX2NhbGwgPSBkcGx5cjo6aWZfZWxzZSgKICAgICAgICAgIGFsbCgKICAgICAgICAgICAgYyhzY2RibF9wcmVkaWN0aW9uLCBzY3J1YmxldF9wcmVkaWN0aW9uLCBjeGRzX3ByZWRpY3Rpb24pID09ICJkb3VibGV0IgogICAgICAgICAgKSwKICAgICAgICAgICJkb3VibGV0IiwgCiAgICAgICAgICAic2luZ2xldCIKICAgICAgICApKSB8PgogICAgICAgIGRwbHlyOjptdXRhdGUoCiAgICAgICAgICBjYWxsX3R5cGUgPSBkcGx5cjo6Y2FzZV93aGVuKAogICAgICAgICAgICBjb25zZW5zdXNfY2FsbCA9PSAiZG91YmxldCIgJiYgZ3JvdW5kX3RydXRoID09ICJkb3VibGV0IiB+ICJ0cCIsCiAgICAgICAgICAgIGNvbnNlbnN1c19jYWxsID09ICJzaW5nbGV0IiAmJiBncm91bmRfdHJ1dGggPT0gInNpbmdsZXQiIH4gInRuIiwKICAgICAgICAgICAgY29uc2Vuc3VzX2NhbGwgPT0gImRvdWJsZXQiICYmIGdyb3VuZF90cnV0aCA9PSAic2luZ2xldCIgfiAiZnAiLAogICAgICAgICAgICBjb25zZW5zdXNfY2FsbCA9PSAic2luZ2xldCIgJiYgZ3JvdW5kX3RydXRoID09ICJkb3VibGV0IiB+ICJmbiIKICAgICAgICAgICksIAogICAgICAgICAgIyBzZXQgYXNzb2NpYXRlZCBwbG90dGluZyBjb2xvcnMKICAgICAgICAgIGNhbGxfdHlwZV9jb2xvciA9IGRwbHlyOjpjYXNlX3doZW4oCiAgICAgICAgICAgIGNhbGxfdHlwZSA9PSAidHAiIH4gdW5uYW1lKGNvbmZ1c2lvbl9jb2xvcnNbInRwIl0pLAogICAgICAgICAgICBjYWxsX3R5cGUgPT0gInRuIiB+IHVubmFtZShjb25mdXNpb25fY29sb3JzWyJ0biJdKSwKICAgICAgICAgICAgY2FsbF90eXBlID09ICJmcCIgfiB1bm5hbWUoY29uZnVzaW9uX2NvbG9yc1siZnAiXSksCiAgICAgICAgICAgIGNhbGxfdHlwZSA9PSAiZm4iIH4gdW5uYW1lKGNvbmZ1c2lvbl9jb2xvcnNbImZuIl0pCiAgICAgICAgICApCiAgICAgICAgICApCiAgICAgIAogICAgICByZXR1cm4oZGF0YXNldF9kZikKICAgIH0KICApCm5hbWVzKGRvdWJsZXRfZGZfbGlzdCkgPC0gZGF0YXNldF9uYW1lcwpgYGAKCgoKCiMjIFVwc2V0IHBsb3RzCgpUaGlzIHNlY3Rpb24gY29udGFpbnMgdXBzZXQgcGxvdHMgdGhhdCBzaG93IG92ZXJsYXAgYWNyb3NzIGRvdWJsZXQgY2FsbHMgZnJvbSBlYWNoIG1ldGhvZCwgZGlzcGxheWVkIGZvciBlYWNoIGRhdGFzZXQuCgpgYGB7cn0KcHVsbF9iYXJjb2RlcyA8LSBmdW5jdGlvbihkZiwgcHJlZF92YXIpIHsKICAjIEhlbHBlciBmdW5jdGlvbiB0byBwdWxsIG91dCBiYXJjb2RlcyBmb3IgZG91YmxldCBjYWxscwogIGRmJGJhcmNvZGVzW2RmW1twcmVkX3Zhcl1dID09ICJkb3VibGV0Il0KfQoKdXBzZXRfbGlzdCA8LSBkb3VibGV0X2RmX2xpc3QgfD4KICBwdXJycjo6aXdhbGsoCiAgICBcKGRmLCBkYXRhc2V0KSB7CiAgICAgIAogICAgICBkb3VibGV0X2JhcmNvZGVzIDwtIGxpc3QoCiAgICAgICAgInNjRGJsRmluZGVyIiA9IHB1bGxfYmFyY29kZXMoZGYsICJzY2RibF9wcmVkaWN0aW9uIiksCiAgICAgICAgInNjcnVibGV0IiAgICA9IHB1bGxfYmFyY29kZXMoZGYsICJzY3J1YmxldF9wcmVkaWN0aW9uIiksCiAgICAgICAgImN4ZHMiICAgICAgICA9IHB1bGxfYmFyY29kZXMoZGYsICJjeGRzX3ByZWRpY3Rpb24iKQogICAgICApCiAgICAgIAogICAgICBVcFNldFI6OnVwc2V0KGZyb21MaXN0KGRvdWJsZXRfYmFyY29kZXMpLCBvcmRlci5ieSA9ICJmcmVxIikgfD4gcHJpbnQoKQogICAgICBncmlkOjpncmlkLnRleHQoICMgcGxvdCB0aXRsZQogICAgICAgIGRhdGFzZXQsCiAgICAgICAgeCA9IDAuNjUsIAogICAgICAgIHkgPSAwLjk1LCAKICAgICAgICBncCA9IGdyaWQ6OmdwYXIoZm9udHNpemU9MTYpCiAgICAgICkgCgogICAgfQogICkKCmBgYAoKCgojIyBFdmFsdWF0aW5nIGNvbnNlbnN1cyBwZXJmb3JtYW5jZQoKVGhpcyBzZWN0aW9uIHZpc3VhbGl6ZXMgYW5kIGV2YWx1YXRlcyB0aGUgY29uc2Vuc3VzIGRvdWJsZXQgY2FsbHMgYWNyb3NzIGVhY2ggZGF0YXNldC4KCgoKIyMjIFBDQQoKVGhpcyBzZWN0aW9uIHBsb3RzIHRoZSBQQ0EgZm9yIGVhY2ggZGF0YXNldCwgd2l0aCB0aHJlZSBjb2xvciBzY2hlbWVzIGZyb20gbGVmdCB0byByaWdodDoKCjEuIEdyb3VuZCB0cnV0aCBkb3VibGV0cyBhcmUgc2hvd24gaW4gYmxhY2sgYW5kIHNpbmdsZXRzIGluIGJsdWUKMi4gQ29uc2Vuc3VzIGRvdWJsZXRzIGFyZSBzaG93biBpbiBibGFjayBhbmQgYWxsIHJlbWFpbmluZyBkcm9wbGV0cyBpbiBibHVlCjMuIFBvaW50cyBhcmUgY29sb3JlZCBiYXNlZCBvbiBjb21wYXJpbmcgdGhlIGNvbnNlbnN1cyBjYWxsIHRvIHRoZSBncm91bmQgdHJ1dGggYXMgb25lIG9mOgogIC0gdHJ1ZSBwb3NpdGl2ZSAoYHRwYCksIHRydWUgbmVnYXRpdmUgKGB0bmApLCBmYWxzZSBwb3NpdGl2ZSAoYGZwYCksIGZhbHNlIG5lZ2F0aXZlIChgZm5gKQoKCmBgYHtyLCBmaWcud2lkdGggPSAxMiwgZmlnLmhlaWdodCA9IDZ9CiMgTWFrZSBhIGxlZ2VuZCBmb3IgdGhlIGNvbmZ1c2lvbi1jb2xvcmVkIFBDQQpsZWdlbmRfcGxvdCA8LSBkYXRhLmZyYW1lKAogIHggPSBmYWN0b3IobmFtZXMoY29uZnVzaW9uX2NvbG9ycyksIGxldmVscyA9IG5hbWVzKGNvbmZ1c2lvbl9jb2xvcnMpKSwgeSA9IDE6NAopIHw+CiBnZ3Bsb3QoYWVzKHggPSB4LCB5ID0geSwgY29sb3IgPSB4KSkgKyAKICBnZW9tX3BvaW50KHNpemUgPSAzKSArIAogIHNjYWxlX2NvbG9yX21hbnVhbChuYW1lID0gIk1ldHJpYyIsIHZhbHVlcyA9IGNvbmZ1c2lvbl9jb2xvcnMpIApjb25mdXNpb25fbGVnZW5kIDwtIGdncHVicjo6Z2V0X2xlZ2VuZChsZWdlbmRfcGxvdCkgfD4gZ2dwdWJyOjphc19nZ3Bsb3QoKQoKZG91YmxldF9kZl9saXN0IHw+CiAgcHVycnI6Oml3YWxrKAogICAgXChkZiwgZGF0YXNldCkgewogICAgICAKICAgICAgIyBGaXJzdCwgZ3JvdW5kIHRydXRoCiAgICAgIHAxIDwtIHBsb3RfcGNhX2NhbGxzKAogICAgICAgIGRmLCAKICAgICAgICBjb2xvcl9jb2x1bW4gPSBncm91bmRfdHJ1dGgsIAogICAgICAgIGNvbG9yX2xhYiA9ICJHcm91bmQgdHJ1dGgiCiAgICAgICkKICAgICAgCiAgICAgICMgU2Vjb25kLCBjb25zZW5zdXMgY2FsbAogICAgICBwMiA8LSBwbG90X3BjYV9jYWxscygKICAgICAgICBkZiwgCiAgICAgICAgY29sb3JfY29sdW1uID0gY29uc2Vuc3VzX2NhbGwsIAogICAgICAgIGNvbG9yX2xhYiA9ICJDb25zZW5zdXMgY2FsbCIKICAgICAgKQogICAgICAKICAgICAgIyBUaGlyZCwgY2FsbCB0eXBlCiAgICAgIHAzIDwtIHBsb3RfcGNhX21ldHJpY3MoCiAgICAgICAgZGYsCiAgICAgICAgY2FsbF90eXBlX2NvbG9yLAogICAgICAgIGZhbHNlX2NvbG9ycyA9IHVubmFtZShjKGNvbmZ1c2lvbl9jb2xvcnNbImZuIl0sIGNvbmZ1c2lvbl9jb2xvcnNbImZwIl0pKQogICAgICApCgogICAgICAjIGNvbWJpbmUgYW5kIHBsb3QKICAgICAgcGxvdCggcDEgKyBwMiArIHAzICsgY29uZnVzaW9uX2xlZ2VuZCArIHBsb3RfYW5ub3RhdGlvbihnbHVlOjpnbHVlKCJQQ0EgZm9yIHtkYXRhc2V0fSIpKSArIHBsb3RfbGF5b3V0KG5jb2w9NCwgd2lkdGhzID0gYygxLDEsMSwwLjI1KSkgKQogICAgfQogICkKYGBgCgoKIyMjIFBlcmZvcm1hbmNlIG1ldHJpY3MgCgpUaGlzIHNlY3Rpb24gY2FsY3VsYXRlcyBhIGNvbmZ1c2lvbiBtYXRyaXggYW5kIGFzc29jaWF0ZWQgc3RhdGlzdGljcyBvbiB0aGUgY29uc2Vuc3VzIGNhbGxzLiAKCmBgYHtyfQptZXRyaWNfZGYgPC0gZG91YmxldF9kZl9saXN0IHw+CiAgcHVycnI6OmltYXAoIAogICAgXChkZiwgZGF0YXNldCkgewogICAgICAgIHByaW50KGdsdWU6OmdsdWUoIj09PT09PT09PT09PT09PT09PT09PT09PSB7ZGF0YXNldH0gPT09PT09PT09PT09PT09PT09PT09PT09IikpCiAgICAgIAogICAgICAgIGNhdCgiVGFibGUgb2YgY29uc2Vuc3VzIGNhbGxzOiIpCiAgICAgICAgcHJpbnQodGFibGUoZGYkY29uc2Vuc3VzX2NhbGwpKQogICAgICAgIAogICAgICAgIGNhdCgiXG5cbiIpCiAgICAgICAgCiAgICAgICAgY29uZnVzaW9uX3Jlc3VsdCA8LSBjYXJldDo6Y29uZnVzaW9uTWF0cml4KAogICAgICAgICAgIyB0cnV0aCBzaG91bGQgYmUgZmlyc3QKICAgICAgICAgIHRhYmxlKAogICAgICAgICAgICAiVHJ1dGgiID0gZGYkZ3JvdW5kX3RydXRoLAogICAgICAgICAgICAiQ29uc2Vuc3VzIHByZWRpY3Rpb24iID0gZGYkY29uc2Vuc3VzX2NhbGwKICAgICAgICAgICksIAogICAgICAgICAgcG9zaXRpdmUgPSAiZG91YmxldCIKICAgICAgICApIAogICAgICAgIAogICAgICAgIHByaW50KGNvbmZ1c2lvbl9yZXN1bHQpCiAgICAgICAgCiAgICAgICAgIyBFeHRyYWN0IGluZm9ybWF0aW9uIHdlIHdhbnQgdG8gcHJlc2VudCBsYXRlciBpbiBhIHRhYmxlCiAgICAgICAgdGliYmxlOjp0aWJibGUoCiAgICAgICAgICAiRGF0YXNldCBuYW1lIiA9IGRhdGFzZXQsCiAgICAgICAgICAiS2FwcGEiID0gcm91bmQoY29uZnVzaW9uX3Jlc3VsdCRvdmVyYWxsWyJLYXBwYSJdLCAzKSwgCiAgICAgICAgICAiQmFsYW5jZWQgYWNjdXJhY3kiID0gcm91bmQoY29uZnVzaW9uX3Jlc3VsdCRieUNsYXNzWyJCYWxhbmNlZCBBY2N1cmFjeSJdLCAzKQogICAgICAgICkKICAgIH0KICApIHw+CiAgZHBseXI6OmJpbmRfcm93cygpCmBgYAoKCgojIyBDb25jbHVzaW9ucwoKT3ZlcmFsbCwgbWV0aG9kcyBkbyBub3QgaGF2ZSBzdWJzdGFudGlhbCBvdmVybGFwIHdpdGggb3V0IGFub3RoZXIuIApUaGV5IGVhY2ggdGVuZCB0byBkZXRlY3QgZGlmZmVyZW50IHNldHMgb2YgZG91YmxldHMsIGxlYWRpbmcgdG8gZmFpcmx5IHNtYWxsIHNldHMgb2YgY29uc2Vuc3VzIGRvdWJsZXRzLiAKRnVydGhlciwgdGhlIGNvbnNlbnN1cyBkb3VibGV0cyBjYWxsZWQgYnkgYWxsIHRocmVlIG1ldGhvZHMgaGF2ZSBzb21lLCBidXQgbm90IHN1YnN0YW50aWFsLCBvdmVybGFwIHdpdGggdGhlIGdyb3VuZCB0cnV0aC4gCgpGb3IgdGhyZWUgb3V0IG9mIGZvdXIgZGF0YXNldHMsIGBzY0RibEZpbmRlcmAgcHJlZGljdHMgYSBtdWNoIGxhcmdlciBudW1iZXIgb2YgZG91YmxldHMgY29tcGFyZWQgdG8gb3RoZXIgbWV0aG9kcy4gCkZvciBgcGR4LU1VTFRJYCwgaG93ZXZlciwgYGN4ZHNgIHByZWRpY3RzIGEgbXVjaCBsYXJnZXIgbnVtYmVyIG9mIGRyb3BsZXRzLgoKVGhlIHRhYmxlIGJlbG93IHN1bW1hcml6ZXMgcGVyZm9ybWFuY2Ugb2YgdGhlICJjb25zZW5zdXMgY2FsbGVyIi4gCk5vdGUgdGhhdCwgaW4gdGhlIFtiZW5jaG1hcmtpbmcgcGFwZXIgdGhlc2UgZGF0YXNldHMgd2VyZSBvcmlnaW5hbGx5IGFuYWx5emVkIGluXShodHRwczovL2RvaS5vcmcvMTAuMTAxNi9qLmNlbHMuMjAyMC4xMS4wMDgpLCBgaG0tNmtgIHdhcyBvYnNlcnZlZCB0byBiZSBvbmUgb2YgdGhlICJlYXNpZXN0IiBkYXRhc2V0cyB0byBjbGFzc2lmeSBhY3Jvc3MgbWV0aG9kcy4gIApDb25zaXN0ZW50IHdpdGggdGhhdCBvYnNlcnZhdGlvbiwgaXQgaGFzIHRoZSBoaWdoZXN0IGBrYXBwYWAgdmFsdWUgaGVyZSwgYWx0aG91Z2ggaXQgaXMgc3RpbGwgZmFpcmx5IGxvdyAtIHRob3VnaCBub3QgYXMgbG93IGFzIHRoZSBvdGhlciBtZXRob2RzLCB3aGljaCBhcmUgdmVyeSBjbG9zZSB0byAwLiAKCmBgYHtyfQptZXRyaWNfZGYKYGBgCgoKIyMgU2Vzc2lvbiBJbmZvCgpgYGB7ciBzZXNzaW9uIGluZm99CiMgcmVjb3JkIHRoZSB2ZXJzaW9ucyBvZiB0aGUgcGFja2FnZXMgdXNlZCBpbiB0aGlzIGFuYWx5c2lzIGFuZCBvdGhlciBlbnZpcm9ubWVudCBpbmZvcm1hdGlvbgpzZXNzaW9uSW5mbygpCmBgYAo=